<?php

    #-------------------------------------------------------------#
	# This file is part of the Tekuna MVC Microframework for PHP. #
	# (c) 2009 Niels Jäckel <niels.jaeckel@tekuna.org>            #
	# Please visit http://tekuna.org/ for more information.       #
	#                                                             #
	# This software is licensed under the MIT License. Please     #
	# view the LICENSE file for the full text of this license.    #
    #-------------------------------------------------------------#


	Tekuna :: load('org.tekuna.core.request.PostFile');
	Tekuna :: load('org.tekuna.core.request.RequestException');
	Tekuna :: load('org.tekuna.core.filter.TekunaFilter');
	Tekuna :: load('org.tekuna.core.filter.data.AlphanumericCharsFilter');
	Tekuna :: load('org.tekuna.core.filter.data.BooleanFilter');
	Tekuna :: load('org.tekuna.core.filter.data.EmptyFilter');
	Tekuna :: load('org.tekuna.core.filter.data.EntityDecodeFilter');
	Tekuna :: load('org.tekuna.core.filter.data.FilterChain');
	Tekuna :: load('org.tekuna.core.filter.data.FloatFilter');
	Tekuna :: load('org.tekuna.core.filter.data.IntegerFilter');
	Tekuna :: load('org.tekuna.core.filter.data.LengthFilter');
	Tekuna :: load('org.tekuna.core.filter.data.NoHTMLFilter');
	Tekuna :: load('org.tekuna.core.filter.data.NoScriptFilter');
	Tekuna :: load('org.tekuna.core.filter.data.NoSpecialCharsFilter');
	Tekuna :: load('org.tekuna.core.filter.data.PHPInternalFilter');
	Tekuna :: load('org.tekuna.core.filter.data.PrintableCharsFilter');
	Tekuna :: load('org.tekuna.core.filter.data.SQLFilter');
	Tekuna :: load('org.tekuna.core.filter.data.TrimFilter');
	Tekuna :: load('org.tekuna.core.filter.data.UnixLineDelimitersFilter');
	Tekuna :: load('org.tekuna.core.filter.data.URLCharsFilter');
	Tekuna :: load('org.tekuna.core.filter.data.XMLSpecialCharsFilter');
	Tekuna :: load('org.tekuna.core.filter.file.EmptyFileFilter');
	Tekuna :: load('org.tekuna.core.filter.file.FileDataFilter');


	/**
	 * Request data abstraction object. Abstracts
	 *
	 *   - GET data
	 *   - POST data
	 *   - uploaded files
	 *
	 * For each single field within the different scopes can be
	 * defined some filters that are evaluated implicitly while
	 * delivering the requested values.
	 */

	class Request {

		protected
			$arrRawGET = array(),
			$arrRawPOST = array(),
			$arrFiles = array(),
			$sRequestProtocol = 'HTTP/1.1',
			$sRequestMethod = '',
			$sImportEncoding = 'UTF-8',
			$sAuthenticatedUser = NULL,
			$sAuthenticatedPassword = NULL,
			$bIsHTTPS = FALSE,

			$arrGETFilters = array(),
			$arrPOSTFilters = array(),
			$arrFileFilters = array(),

			$objDefaultFilter = NULL,
			$objDefaultFileFilter = NULL;


		/**
		 * Request's constructor. The values of the superglobal arrays
		 * for GET, POST, REQUEST and FILES are imported and resetted
		 * afterwards. The internal encoding is set to UTF-8. The effects
		 * of magic quotes (runtime and gpc) are removed to privide
		 * configuration-independent "clean" values.
		 *
		 * For setting the internal encoding, the mbstring extension is
		 * needed. If the extension is not loaded, a RequestException
		 * is thrown.
		 */

		public function __construct() {

			global $HTTP_GET_VARS;
			global $HTTP_POST_VARS;
			global $HTTP_REQUEST_VARS;
			global $HTTP_FILES_VARS;

			// check for the mb extension
			if (! extension_loaded('mbstring')) {

				throw new RequestException("The 'mbstring' extension is not loaded. This is required for Tekuna because of the real UTF-8 support.");
			}

			// set UTF-8 as default internal encoding
			mb_internal_encoding('UTF-8');

			// turn off magic_quotes_runtime
			set_magic_quotes_runtime(0);

			// import the raw values
			$this -> arrRawGET = $_GET;
			$this -> arrRawPOST = $_POST;

			// store request protocol if given
			if (isset($_SERVER['SERVER_PROTOCOL'])) {

				$this -> sRequestProtocol = $_SERVER['SERVER_PROTOCOL'];
			}

			// set request method
			$this -> sRequestMethod = strtoupper($_SERVER['REQUEST_METHOD']);

			// set authenticated user and password if given
			if (isset($_SERVER['PHP_AUTH_USER'])) {

				$this -> sAuthenticatedUser = $_SERVER['PHP_AUTH_USER'];
			}

			if (isset($_SERVER['PHP_AUTH_PW'])) {

				$this -> sAuthenticatedPassword = $_SERVER['PHP_AUTH_PW'];
			}

			if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {

				$this -> bIsHTTPS = TRUE;
			}

			// set default filters
			$this -> objDefaultFilter = new NoSpecialCharsFilter();
			$this -> objDefaultFileFilter = new EmptyFileFilter();

			// import file data
			$this -> importUploadedFiles();

			// clear raw values
			$_GET = array();
			$_POST = array();
			$_REQUEST = array();
			$_FILES = array();

			$HTTP_GET_VARS = array();
			$HTTP_POST_VARS = array();
			$HTTP_REQUEST_VARS = array();
			$HTTP_FILES_VARS = array();

			// clear raw POST array if available
			if (isset($GLOBALS['HTTP_RAW_POST_DATA'])) {

				$GLOBALS['HTTP_RAW_POST_DATA'] = array();
			}
		}


		protected function importUploadedFiles() {

			if (isset($_FILES) && is_array($_FILES)) {

				// import files
				foreach ($_FILES as $sID => $arrData) {

					if ($arrData['error'] != UPLOAD_ERR_NO_FILE /* = 4 */) {

						// correct \' to ' on Linux with magic_quotes_gpc enabled
						if (get_magic_quotes_gpc()) {

							$arrData['name'] = stripslashes($arrData['name']);
						}

						$this -> arrFiles[$sID] = new PostFile($sID, $arrData);
					}
					else {

						throw new RequestException("No valid file was uploaded for file field '$sID'.");
					}
				}
			}
		}


		/**
		 * @return boolean returns TRUE if the request was performed by HTTPS.
		 */

		public function isHTTPSRequest() {

			return $this -> bIsHTTPS;
		}


		/**
		 * @return string returns the request protocol and revision (e.g. HTTP/1.1).
		 */

		public function getRequestProtocol() {

			return $this -> sRequestProtocol;
		}


		/**
		 * @return string returns the request method of the current request; value is GET or POST
		 */

		public function getRequestMethod() {

			return $this -> sRequestMethod;
		}


		/**
		 * @return boolean returns true if the current request is performed by GET.
		 */

		public function isGETRequest() {

			return ($this -> sRequestMethod === 'GET');
		}


		/**
		 * @return boolean returns true if the current request is performed by POST.
		 */

		public function isPOSTRequest() {

			return ($this -> sRequestMethod === 'POST');
		}


		/**
		 * Set the encoding of the request data. This value defaults to UTF-8.
		 * If another encoding is set, the data is converted to the internally
		 * used UTF-8 encoding.
		 *
		 * @param string $sEncoding the encoding of the request data
		 */

		public function setImportEncoding($sEncoding) {

			$this -> sImportEncoding = $sEncoding;
		}


		/**
		 * @return string returns the currently set import encoding
		 */

		public function getImportEncoding() {

			return $this -> sImportEncoding;
		}


		/**
		 * @return boolean returns true if there is basic user authentication data available.
		 */

		public function hasAuthenticatedUser() {

			return ($this -> sAuthenticatedUser !== NULL);
		}


		/**
		 * @return string returns the user name of the authenticated user
		 */

		public function getAuthenticatedUser() {

			return $this -> sAuthenticatedUser;
		}


		/**
		 * @return string returns the password of the authenticated user
		 */

		public function getAuthenticatedPassword() {

			return $this -> sAuthenticatedPassword;
		}


		/**
		 * Set a filter object for a certain field in the GET scope.
		 *
		 * @param string $sField the field in the GET scope
		 * @param TekunaFilter a filter object
		 */

		public function setGETFilter($sField, TekunaFilter $objFilter) {

			$this -> arrGETFilters[$sField] = $objFilter;
		}


		/**
		 * Returns the filter for a specific field in the GET scope. If
		 * no special filter is set (and implicitly the default filter
		 * would be applied) this method returns NULL.
		 *
		 * @param string $sField the field in the GET scope
		 * @return TekunaFilter object or NULL
		 */

		public function getGETFilter($sField) {

			if (isset($this -> arrGETFilters[$sField])) {

				return $this -> arrGETFilters[$sField];
			}
			else {

				return NULL;
			}
		}


		/**
		 * Set a filter object for a certain field in the POST scope.
		 *
		 * @param string $sField the field in the POST scope
		 * @param TekunaFilter a filter object
		 */

		public function setPOSTFilter($sField, TekunaFilter $objFilter) {

			$this -> arrPOSTFilters[$sField] = $objFilter;
		}


		/**
		 * Returns the filter for a specific field in the POST scope. If
		 * no special filter is set (and implicitly the default filter
		 * would be applied) this method returns NULL.
		 *
		 * @param string $sField the field in the POST scope
		 * @return TekunaFilter object or NULL
		 */

		public function getPOSTFilter($sField) {

			if (isset($this -> arrPOSTFilters[$sField])) {

				return $this -> arrPOSTFilters[$sField];
			}
			else {

				return NULL;
			}
		}


		/**
		 * Set a filter object for an uploaded file
		 *
		 * @param string $sFile the identifier of the file
		 * @param TekunaFilter a filter object for this file
		 */

		public function setFileFilter($sFile, TekunaFileFilter $objFilter) {

			$this -> arrFileFilters[$sFile] = $objFilter;
		}


		/**
		 * Returns the filter for a specific uploaded file. If no
		 * special filter is set (and implicitly the default file
		 * filter would be applied) NULL is returned.
		 *
		 * @param string $sFile the identifier of the file
		 * @return TekunaFileFilter object or NULL
		 */

		public function getFileFilter($sFile) {

			if (isset($this -> arrFileFilters[$sFile])) {

				return $this -> arrFileFilters[$sFile];
			}
			else {

				return NULL;
			}
		}


		/**
		 * Set the default filter that is applied, if no special filter
		 * for a specific GET or POST field is set.
		 *
		 * @param TekunaFilter $objFilter the new filter object
		 */

		public function setDefaultFilter(TekunaFilter $objFilter) {

			$this -> objDefaultFilter = $objFilter;
		}


		/**
		 * Set the default filter that is applied, if no special filter
		 * for a specific uploaded file is set
		 *
		 * @param TekunaFileFilter $objFilter the new filter object
		 */

		public function setDefaultFileFilter(TekunaFileFilter $objFilter) {

			$this -> objDefaultFileFilter = $objFilter;
		}


		/**
		 * @return TekunaFilter returns the current active default filter for GET and POST values
		 */

		public function getDefaultFilter() {

			return $this -> objDefaultFilter;
		}


		/**
		 * @return TekunaFileFilter returns the current active default file filter for uploaded files.
		 */

		public function getDefaultFileFilter() {

			return $this -> objDefaultFileFilter;
		}


		/**
		 * Checks if a specific value in the GET scope is available.
		 *
		 * @param string $sField the field in the GET scope
		 * @return boolean returns true if the value is available
		 */

		public function isGETValueAvailable($sField) {

			return isset($this -> arrRawGET[$sField]);
		}


		/**
		 * Returns a value from the GET scope. If the value is not available
		 * a RequestException is thrown. The defined filter (or the default filter)
		 * is applied to the value and (if it is an array) all values in any depth.
		 *
		 * @param string $sField the field in the GET scope
		 * @return string the value of this field
		 */

		public function getGETValue($sField) {

			if (! isset($this -> arrRawGET[$sField])) {

				throw new RequestException("Key '$sField' not available in GET scope.");
			}

			$objFilter = NULL;
			if (isset($this -> arrGETFilters[$sField])) {

				$objFilter = $this -> arrGETFilters[$sField];
			}
			else {

				$objFilter = $this -> objDefaultFilter;
			}

			return $this -> applyFilterRecursive($this -> arrRawGET[$sField], $objFilter);
		}


		/**
		 * @return array returns an associative array of all available values in the GET scope.
		 */

		public function getAllGETValues() {

			$arrReturn = array();

			foreach (array_keys($this -> arrRawGET) as $sField) {

				$arrReturn[$sField] = $this -> getGETValue($sField);
			}

			return $arrReturn;
		}


		/**
		 * Checks if a specific value in the POST scope is available.
		 *
		 * @param string $sField the field in the POST scope
		 * @return boolean returns true if the value is available
		 */

		public function isPOSTValueAvailable($sField) {

			return isset($this -> arrRawPOST[$sField]);
		}


		/**
		 * Returns a value from the POST scope. If the value is not available
		 * a RequestException is thrown. The defined filter (or the default filter)
		 * is applied to the value and (if it is an array) all values in any depth.
		 *
		 * @param string $sField the field in the POST scope
		 * @return string the value of this field
		 */

		public function getPOSTValue($sField) {

			if (! isset($this -> arrRawPOST[$sField])) {

				throw new RequestException("Key '$sField' not available in POST scope.");
			}

			$objFilter = NULL;
			if (isset($this -> arrPOSTFilters[$sField])) {

				$objFilter = $this -> arrPOSTFilters[$sField];
			}
			else {

				$objFilter = $this -> objDefaultFilter;
			}

			return $this -> applyFilterRecursive($this -> arrRawPOST[$sField], $objFilter);
		}


		/**
		 * @return array returns an associative array of all available values in the POST scope.
		 */

		public function getAllPOSTValues() {

			$arrReturn = array();

			foreach (array_keys($this -> arrRawPOST) as $sField) {

				$arrReturn[$sField] = $this -> getPOSTValue($sField);
			}

			return $arrReturn;
		}


		/**
		 * Checks if a specific uploaded file is available.
		 *
		 * @param string $sFileID the file identifier
		 * @return boolean returns true if the value is available
		 */

		public function isFileAvailable($sFileID) {

			return isset($this -> arrFiles[$sFileID]);
		}


		/**
		 * Returns PostFile object of an uploaded file. If the file is not available
		 * a RequestException is thrown. The defined filter (or the default file filter)
		 * is applied to the value.
		 *
		 * @param string $sFileID the identifier of the file
		 * @return PostFile the file object
		 */

		public function getFile($sFileID) {

			if (! isset($this -> arrFiles[$sFileID])) {

				throw new RequestException("File '$sFileID' does not exist.");
			}

			$objFilter = NULL;
			if (isset($this -> arrFileFilters[$sFileID])) {

				$objFilter = $this -> arrFileFilters[$sFileID];
			}
			else {

				$objFilter = $this -> objDefaultFileFilter;
			}

			return $objFilter -> filter($this -> arrFiles[$sFileID]);
		}


		/**
		 * @return array returns an associative array of all available uploaded files.
		 */

		public function getAllFiles() {

			$arrReturn = array();

			foreach (array_keys($this -> arrFiles) as $sFileID) {

				$arrReturn[$sFileID] = $this -> getFile($sFileID);
			}

			return $arrReturn;
		}


		protected function applyFilterRecursive($arrData, TekunaFilter $objFilter) {

			if (is_array($arrData)) {

				$arrReturn = array();

				foreach ($arrData as $mKey => $mValue) {

					$mValue = $this -> applyFilterRecursive($mValue, $objFilter);

					$arrReturn[$mKey] = $mValue;
				}

				return $arrReturn;
			}
			else {

				// adjust encoding of the data if necessary
				if ($this -> getImportEncoding() != mb_internal_encoding()) {

					$arrData = mb_convert_encoding($arrData, mb_internal_encoding(), $this -> getImportEncoding());
				}

				// decode escaped values if magic_quotes_gpc is enabled
				if (get_magic_quotes_gpc()) {

					$arrData = stripslashes($arrData);
				}

				// filter the data
				return $objFilter -> filter($arrData);
			}
		}
	}

